到 Day 20 為止,每一次呼叫 Gemini 都是我們先把資料查好、整理成文字或圖片再交給它,Day 09 交的是異常摘要,Day 16 交的是廣告圖,模型只負責讀和寫,要看哪些資料是我們事先決定的,這種做法在題目固定的時候很好用,可是同事隨口問一句「上個月 TikTok 廣告花了多少錢」,沒有人事先替模型準備好這一題的資料。
我把這個問題直接丟給 gemini-3.6-flash,沒有附任何資料,它回答「上個月 TikTok 廣告的總支出為新台幣 125,000 元,整體 ROAS 表現為 3.5」,還補了一句詳細數據已整理在行銷月報資料夾中,這個品牌從頭到尾沒有投過 TikTok,資料庫裡一筆都沒有,125,000 和 3.5 都是它自己編的,而且語氣和真的查過一模一樣,跟它生氣也沒用只能另外想方法。
今天要做的是 Function Calling,把「查資料」這件事寫成工具交給 Gemini,讓它自己判斷這個問題要不要查、查哪一個、參數填什麼,查回來的數字再由它整理成回答。
今日核心目標:
| 步驟 | 誰做的 | 做什麼 |
|---|---|---|
| 1 | 程式 | 把問題和三個工具的定義一起送給 Gemini |
| 2 | Gemini | 不直接回答,回傳「我要呼叫哪個工具、參數是什麼」 |
| 3 | 程式 | 檢查參數,用寫好的 SQL 查 BigQuery |
| 4 | 程式 | 把查詢結果連同模型上一輪的內容一起送回去 |
| 5 | Gemini | 根據查到的資料寫出回答 |
💡 核心工程理念:
一個工具在模型眼中只有三樣東西,下面是查歸因的那一個,為了好讀只節錄一部分,日期和排除直接流量兩種參數沒有列出來:
{
"name": "get_channel_attribution",
"description": "查各個流量來源(通路)在一段期間內分到多少訂單功勞與營收,"
"用來回答「哪個通路帶來最多訂單」這類問題。"
"資料期間是 2026-06-19 到 2026-09-16。",
"parameters": {
"type": "OBJECT",
"properties": {
"attribution_model": {
"type": "STRING",
"enum": ["first_touch", "last_touch", "time_decay"],
"description": "功勞怎麼分:first_touch 全部算給第一次接觸、"
"last_touch 全部算給下單前最後一次接觸、"
"time_decay 越接近下單分到越多",
},
},
"required": ["attribution_model"],
},
}
| 部分 | 給模型的訊息 | 寫的時候要注意 |
|---|---|---|
| 名稱 | 這個工具叫什麼 | 用動詞開頭的英文,一看就知道是查什麼 |
| 說明 | 什麼情況該用它 | 寫出它能回答哪一類問題,以及資料的範圍 |
| 參數 | 可以填什麼 | 能列舉的就用 enum 列出來,每個選項的意思寫清楚 |
模型看不到函式裡的程式,也看不到資料表,它決定要不要用這個工具的依據只有說明文字,所以說明其實是寫給模型看的提示,「用來回答哪個通路帶來最多訂單這類問題」這種句子比「歸因查詢」有用得多,參數的 enum 也是同樣的道理,使用者說的是「最終點擊」,模型要能對到 last_touch,靠的就是那一行選項說明。
今天的三個工具:
| 工具 | 讀的表 | 參數 |
|---|---|---|
get_channel_attribution |
mart_attribution(Day 08) |
歸因算法、起訖日期、要不要排除直接流量 |
get_anomaly_diagnosis |
diag_summary、mart_diagnosis(Day 09) |
通路 |
get_creative_feature_lift |
mart_creative_lift(Day 17) |
指標(點擊率或轉換率) |
以「6 月 19 日到 9 月 16 日這段期間,用時間衰減的算法,哪個通路分到的訂單功勞最多」這一題為例,第一次呼叫模型,它沒有回任何文字,回的是這一段:
{"name": "get_channel_attribution",
"args": {"attribution_model": "time_decay", "start_date": "2026-06-19", "end_date": "2026-09-16"}}
題目裡的「時間衰減」被它對到 time_decay,「6 月 19 日到 9 月 16 日」被它補上年份轉成兩個日期,工具說明裡寫了資料期間是 2026 年,程式收到之後檢查參數、查 BigQuery,把六個通路的結果交回去,第二次呼叫模型才得到回答:
在時間衰減模型下,分到最多訂單功勞的通路是 (direct) / (none)(直接流量),約分得 620 筆訂單。若排除直接流量,付費廣告通路中則以 meta / paid_social 分到的功勞最多,約 597 筆訂單。
620 和 597 都對得上資料表(620.4 與 596.6),所以需要查資料的問題至少要呼叫模型兩次,第一次拿到要求,第二次拿到回答。
用 Google Gen AI SDK 寫這一段有兩個地方要留意,第一是 SDK 預設會自動執行函式,把 Python 函式直接放進 tools 它就會自己呼叫、自己把結果送回去,寫起來最省事,但中間每一步都看不到也記不下來,今天的程式把這個功能關掉,改成自己處理每一輪,第二是把結果送回去的時候,模型上一輪回傳的內容要原封不動放回對話裡,那裡面除了函式呼叫還帶著模型思考過程的簽章,自己重新組一份會少掉這一段,下面是簡化後的寫法:
resp = client.models.generate_content(model=MODEL, contents=contents, config=config)
contents.append(resp.candidates[0].content) # 模型這一輪的內容原樣放回去
contents.append(types.Content(role="user", parts=[
types.Part.from_function_response(name=f.name, response=result)
for f, result in results])) # 再接上每個工具查到的結果
另一種做法是只給一個工具叫「執行 SQL」,把資料表結構告訴模型,讓它自己寫查詢,彈性大很多,今天沒有這樣做,原因有三個:
SELECT,也寫得出掃整張事件表的查詢,或是讀到不該給它看的欄位寫死 SQL 之後,程式這一側還有四道檢查,都在 agent/tools.py 裡:
| 檢查 | 做法 |
|---|---|
| 參數白名單 | 歸因算法、通路、指標只收列舉裡的值,日期只收 YYYY-MM-DD,多給的參數直接拒絕 |
| 不拼接字串 | 值用查詢參數帶進 SQL,欄位名稱來自程式裡的對照表,不是模型給的字串 |
| 掃描量上限 | 每次查詢最多掃 100 MB,超過就失敗、不收費 |
| 回傳列數上限 | 最多回 20 列,結果越長下一輪的輸入 Token 越多 |
參數沒通過檢查時程式不會去查 BigQuery,只把錯誤訊息當成工具的結果回給模型,讓它自己修正或是告訴使用者查不到。
六個問題分四種:三題各需要一個工具,一題要用到兩個工具,一題是觀念題不需要查,一題是工具答不了的(就是前言那題 TikTok),模型用 gemini-3.6-flash,溫度設 0,系統指示只有一句「你是電商品牌的行銷資料助理,用繁體中文、三句話以內回答同事的問題」,沒有額外叮嚀它不要亂猜。
有工具的結果:
| 題目 | 模型要求的工具與參數 | 回答 |
|---|---|---|
| 時間衰減下哪個通路功勞最多 | 歸因,time_decay |
直接流量約 620 筆,對 |
| meta 的成效出過哪些狀況 | 診斷,meta |
素材疲乏、競價變貴,另外提到全站的追蹤碼失效,對 |
| 有人物的圖點擊率會不會比較高 | 素材成效,ctr |
1.282 倍,對 |
| meta 兩種算法的功勞各多少,加上異常原因 | 歸因兩次(last_touch、time_decay)加診斷一次 |
380 筆與 596.6 筆,原因也對 |
| 時間衰減是什麼意思 | 沒有呼叫 | 直接解釋 |
| 上個月 TikTok 廣告花了多少 | 沒有呼叫 | 回答沒有 TikTok 的費用資料,無法提供金額 |
四題需要查資料的都選對工具、參數也對,回答裡的數字和原因全部對得上資料表,第四題值得多看一眼,它在同一輪就一口氣提出三個呼叫,歸因查兩次、診斷查一次,程式依序查完一起交回去,所以這一題和其他題一樣只呼叫了模型兩次,觀念題它判斷不需要查就直接回答,TikTok 那題它回答現有資料只涵蓋 Meta、Google 和 LINE,沒有硬查也沒有編數字。
沒有工具的結果:
| 題目 | 回答摘要 | 和資料表對照 |
|---|---|---|
| 時間衰減下哪個通路功勞最多 | 說明自己無法存取報表數據,請對方提供資料,另外補了一句通則說最接近下單的點擊通路通常分最多 | 沒有編數字,但那句通則和資料表不符,實際最多的是直接流量 |
| meta 的成效出過哪些狀況 | iOS 14.5 隱私政策、API 異常、演算法調整,最後提到市場競價加劇與素材疲勞 | 前三項資料表裡沒有,後兩項是泛稱,沒有說是哪個廣告群組、哪一週 |
| 有人物的圖點擊率會不會比較高 | 平均能提升約 20% 到 50% | 實際是 1.282 倍,這個範圍是泛稱不是這個品牌的數字 |
| meta 兩種算法的功勞各多少 | 最終點擊 1,250 筆、時間衰減 980 筆,原因是 Pixel 追蹤碼更新與 iOS 歸因延遲 | 實際是 380 與 596.6,數字和原因都是編的 |
| 時間衰減是什麼意思 | 正確解釋 | 不需要資料 |
| 上個月 TikTok 廣告花了多少 | 新台幣 125,000 元,ROAS 3.5 | 資料庫沒有 TikTok |
六題裡只有第一題老實說查不到,另外有四題給了資料表裡沒有的內容,其中兩題編出了看起來很精確的數字,最麻煩的是這些回答讀起來都很合理,1,250 和 980 連大小關係都和真實資料相反,不對照資料表根本看不出來,同一個模型對第一題會說查不到、對第四題卻直接給數字,沒有工具的時候它會不會猜是沒辦法預期的。
這一輪每題每種情況只問一次,換個問法或再問一次,沒有工具那一側的結果可能不一樣,但有工具時數字來自查詢結果、沒有工具時數字只能來自模型自己,這一點不會變。
count_tokens 也不收費run.sh 會先用這三個上限算出最壞金額印出來,輸入 yes 才開始,問過而且成功的題目不會再問| 情況 | 題數 | 呼叫模型 | 輸入 Token | 輸出加思考 Token | 費用 | 換算每 1,000 題 |
|---|---|---|---|---|---|---|
| 有工具 | 6 | 10 次 | 8,839 | 1,542 | 新台幣 0.40 元 | 新台幣 66 元 |
| 沒有工具 | 6 | 6 次 | 272 | 3,924 | 新台幣 0.48 元 | 新台幣 80 元 |
事前印出的最壞金額是新台幣 10.25 元、預期約 0.86 元,實際兩種情況合計 0.87 元。
工具的成本主要在輸入,三個工具的定義大約 600 個 Token,每一次呼叫模型都要重新送一次,不管這一題用不用得到,觀念題在有工具時輸入是 647 個 Token,沒有工具時只有 43 個,查回來的結果也算輸入,用到三份結果的第四題第二次呼叫的輸入是 1,757 個 Token,所以工具的說明要寫清楚但不要寫成文件,回傳的欄位和列數也要節制。
沒有工具那一側反而比較貴是我沒有預期到的,它的輸入少了三十幾倍,可是思考 Token 用了 3,364 個,有工具時只有 907 個,看起來手上沒有資料的時候模型反而想得比較久,這只是六題各問一次的結果,不能當成定律,但至少說明加上工具不一定比較花錢,費用依 gemini-3.6-flash 在 global 端點的單價(每百萬 Token 輸入 0.75、輸出 3.75 美元)與實際 Token 數算出,匯率以 1 美元約 32 元計,實際以帳單為準。
~/ai-driven-martech-pipeline 執行 git pull,取得 Day 21 的 agent/ 目錄gcloud config get-value project 要印出你的專案 IDbash attribution/run.sh)、Day 09(bash diagnosis/run.sh)與 Day 17(bash lift/run.sh),今天的三個工具讀的是這三天建好的表,Token 用量表 ops_llm_usage 是 Day 16(bash features/run.sh)建的,也要先有google-genai 與 google-cloud-bigquery 兩個 Python 套件,缺少時 run.sh 會提示安裝指令cd ~/ai-driven-martech-pipeline && git pull && bash agent/run.sh
run.sh 先確認前幾天的表都在、問題和工具定義沒有未 commit 的修改,接著做一次不花錢的預檢,照最長那一題實際查一次表、數一次 Token,確認放得進輸入上限,印出最壞金額與預期金額之後停下來,輸入 yes 才開始逐題問,最後是對答案、10 項檢查和五段報表,全部跑完大約三分鐘。
cd ~/ai-driven-martech-pipeline/agent
python3 -c "
import json, tools
from google.cloud import bigquery
bq = bigquery.Client(location='US')
print(json.dumps(tools.run_tool(bq, 'get_channel_attribution', {'attribution_model': 'last_touch'})[0], ensure_ascii=False, indent=1))
print(tools.run_tool(bq, 'get_channel_attribution', {'attribution_model': 'linear'})[0])
"
第一行印出六個通路在最終點擊下的功勞,第二行故意給一個不在清單裡的算法,會印出錯誤訊息而且不會查 BigQuery,這一步不花錢,可以先確認工具本身是對的。
cd ~/ai-driven-martech-pipeline
GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project) python3 agent/ask.py
這一步會呼叫 Gemini,12 題次約新台幣 0.9 元,一樣會先印估價再問,已經問過的題目不會再問。
bq query --nouse_legacy_sql --format=pretty < agent/check.sql
bq query --nouse_legacy_sql --format=pretty --max_rows=200 < agent/report.sql
check.sql 的 10 項都是 OK,包含 12 題次都有回答、沒有工具那一側的工具呼叫次數是 0、每次輸入都在上限內tools_ok 與 args_ok 都是 true,第 2 段看得到模型每一次要求的工具與參數fc_calls_log、fc_tool_log、fc_answers 和 mart_fc_score 四張表都很小,最多的也只有 16 列,留著不會產生費用,明天的助理會沿用 agent/tools.py 的三個工具,ops_llm_usage 裡今天的 16 列 Day 25 做成本儀表板時會用到,都不要刪。
今天把 Day 08 的歸因、Day 09 的異常診斷和 Day 17 的素材成效寫成三個查詢工具交給 Gemini,同樣六個問題,有工具時四題需要查資料的都選對工具、參數正確、數字對得上資料表,不需要查的不查,查不到的說查不到,沒有工具時有四題給了資料表裡沒有的內容,包含前言那筆不存在的 TikTok 花費,12 題次合計約新台幣 0.87 元。
回到篇名,模型會瞎猜不是因為它不夠聰明,是因為它手上沒有資料又被問了需要資料的問題,把查詢的工具交給它之後,數字的來源從模型自己變成資料庫,而工具能查什麼、參數能填什麼、一次能掃多少,都還是寫在我們的程式裡。
明日預告:Day 22《不用寫 SQL,跟 AI 聊天就能查出上週哪支廣告最貴》,今天是一問一答,明天把這三個工具接成可以連續對話的行銷助理,讓它記得前面聊過什麼、接著往下問。